AllLife Bank is a US bank that has a growing customer base. The majority of these customers are liability customers (depositors) with varying sizes of deposits. The number of customers who are also borrowers (asset customers) is quite small, and the bank is interested in expanding this base rapidly to bring in more loan business and in the process, earn more through the interest on loans. In particular, the management wants to explore ways of converting its liability customers to personal loan customers (while retaining them as depositors).
A campaign that the bank ran last year for liability customers showed a healthy conversion rate of over 9% success. This has encouraged the retail marketing department to devise campaigns with better target marketing to increase the success ratio.
You as a Data scientist at AllLife bank have to build a model that will help the marketing department to identify the potential customers who have a higher probability of purchasing the loan.
To predict whether a liability customer will buy personal loans, to understand which customer attributes are most significant in driving purchases, and identify which segment of customers to target more.
ID: Customer IDAge: Customer’s age in completed yearsExperience: #years of professional experienceIncome: Annual income of the customer (in thousand dollars)ZIP Code: Home Address ZIP code.Family: the Family size of the customerCCAvg: Average spending on credit cards per month (in thousand dollars)Education: Education Level. 1: Undergrad; 2: Graduate;3: Advanced/ProfessionalMortgage: Value of house mortgage if any. (in thousand dollars)Personal_Loan: Did this customer accept the personal loan offered in the last campaign? (0: No, 1: Yes)Securities_Account: Does the customer have securities account with the bank? (0: No, 1: Yes)CD_Account: Does the customer have a certificate of deposit (CD) account with the bank? (0: No, 1: Yes)Online: Do customers use internet banking facilities? (0: No, 1: Yes)CreditCard: Does the customer use a credit card issued by any other Bank (excluding All life Bank)? (0: No, 1: Yes)# Installing the libraries with the specified version.
!pip install numpy==1.25.2 pandas==1.5.3 matplotlib==3.7.1 seaborn==0.13.1 scikit-learn==1.2.2 sklearn-pandas==2.2.0 -q --user
Note: After running the above cell, kindly restart the notebook kernel and run all cells sequentially from the start again.
# Libraries to help with reading and manipulating data
import pandas as pd
import numpy as np
# libaries to help with data visualization
import matplotlib.pyplot as plt
import seaborn as sns
# Removes the limit for the number of displayed columns
pd.set_option("display.max_columns", None)
# Sets the limit for the number of displayed rows
pd.set_option("display.max_rows", 200)
# Library to split data
from sklearn.model_selection import train_test_split
# To build model for prediction
from sklearn.tree import DecisionTreeClassifier
from sklearn import tree
# To tune different models
from sklearn.model_selection import GridSearchCV
# To get diferent metric scores
from sklearn.metrics import (
f1_score,
accuracy_score,
recall_score,
precision_score,
confusion_matrix,
make_scorer,
)
import warnings
warnings.filterwarnings("ignore")
from google.colab import drive
drive.mount('/content/drive')
loan= pd.read_csv('/content/drive/MyDrive/Python_Course/P2/Loan_Modelling.csv')
data = loan.copy()
data.head()
data.tail()
Above are the first 5 and last 5 rows of the data.
data.shape
data.info()
data.describe().T
missing_values = data.isnull().sum()
print(missing_values)
There are no missing values in this data set.
Questions:
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(15, 10))
features = data.select_dtypes(include=['number']).columns.tolist()
n_features = len(features)
n_cols = 3
n_rows = -(-n_features // n_cols)
for i, feature in enumerate(features):
plt.subplot(n_rows, n_cols, i + 1)
sns.histplot(data=data, x=feature)
plt.tight_layout()
plt.show()
import matplotlib.pyplot as plt
import seaborn as sns
# Define the figure size
plt.figure(figsize=(15, 10))
# Define the list of numerical features to plot
features = data.select_dtypes(include=['number']).columns.tolist()
# Determine the number of rows and columns needed
n_features = len(features)
n_cols = 3 # Number of columns for subplots
n_rows = -(-n_features // n_cols) # Calculate rows, equivalent to math.ceil(n_features / n_cols)
# Create the box plots
for i, feature in enumerate(features):
plt.subplot(n_rows, n_cols, i + 1) # Adjust subplot grid size
sns.boxplot(data=data, x=feature) # Plot the box plot
plt.tight_layout() # Add spacing between plots
plt.show()
plt.figure(figsize=(8, 6))
sns.boxplot(data=data, x='Mortgage') # Replace 'mortgage_amount' with your actual column name
plt.title('Box Plot for Mortgage')
plt.show()
sns.histplot(data=data, x='Mortgage', kde=True, color='skyblue') # Histogram with KDE (Kernel Density Estimate)
More than half seem to have no Mortgage
data.CreditCard.value_counts()
1470 customers have credit cards.
sns.pairplot(data);
Q3)What are the attributes that have a strong correlation with the target
# defining the figure size
plt.figure(figsize=(10, 7))
# plotting the correlation heatmap
sns.heatmap(data.corr(numeric_only = True), annot=True, fmt='0.2f', cmap='coolwarm');
Income , CCAvg, CD_Account
Q4) How does a customer's interest in purchasing a loan vary with their age?
import pandas as pd
import matplotlib.pyplot as plt
# Define age intervals and corresponding group names
age_intervals = [20, 30, 40, 50, 60, 70]
age_labels = ['20-29', '30-39', '40-49', '50-59', '60-69']
# Categorize the 'Age' column into the defined age groups
data['Age_Category'] = pd.cut(data['Age'], bins=age_intervals, labels=age_labels, right=False)
# Calculate the percentage of customers interested in loans within each age category
loan_interest_by_age_group = data.groupby('Age_Category')['Personal_Loan'].mean()
# Display the interest in loans by age group
print("Customer Interest in Loans by Age Group")
print("-" * 60)
for group, interest in loan_interest_by_age_group.items():
print(f"Age Group: {group}, Interest Rate: {interest:.2f}")
# Generate a bar chart to visualize the data
plt.figure(figsize=(10, 6))
loan_interest_by_age_group.plot(kind='bar', color='blue')
plt.xlabel('Age Group')
plt.ylabel('Percentage of Customers Interested in Loans')
plt.title('Loan Interest by Age Group')
plt.xticks(rotation=30) # Adjust x-axis label rotation for better viewing
plt.tight_layout() # Ensure labels and title fit well
plt.show()
Q5 )How does a customer's interest in purchasing a loan vary with their education?
plt.figure(figsize=(10, 6))
sns.barplot(x='Education', y='Personal_Loan', data=data, estimator=lambda x: x.mean(), palette='coolwarm')
# Set the labels and title for the plot
plt.xlabel('Education Level')
plt.ylabel('Proportion of Customers Interested in Loans')
plt.title('Customer Interest in Purchasing Loans by Education Level')
plt.xticks(rotation=45) # Rotate x-axis labels for readability
plt.tight_layout() # Adjust layout for better fitting
# Display the plot
plt.show()
People with higher education levels seem to be more interested in loans
plt.figure(figsize=(10, 6))
sns.barplot(x='CreditCard', y='Personal_Loan', data=data, estimator=lambda x: x.mean(), palette='coolwarm')
plt.show()
Having a credit card or not doe snot seem to affect Personal loan
plt.figure(figsize=(10, 6))
sns.barplot(x='Online', y='Personal_Loan', data=data, estimator=lambda x: x.mean(), palette='coolwarm')
plt.show()
plt.figure(figsize=(10, 6))
sns.barplot(x='CD_Account', y='Personal_Loan', data=data, estimator=lambda x: x.mean(), palette='coolwarm')
plt.show()
People who have a cd_account are more likely to accept personal_loan
plt.figure(figsize=(10, 6))
sns.barplot(x='Securities_Account', y='Personal_Loan', data=data, estimator=lambda x: x.mean(), palette='coolwarm')
plt.show()
plt.figure(figsize=(8, 5))
sns.barplot(x='Personal_Loan', y='Income', data=data, estimator=lambda x: x.mean(), palette='Blues')
plt.xlabel('Personal Loan Status')
plt.ylabel('Average Income')
plt.title('Average Income by Personal Loan Status')
plt.xticks([0, 1], ['No Loan', 'Loan']) # Labeling the x-axis for clarity
plt.show()
The Avergage income of people who took a loan is higher than those who dont.
plt.figure(figsize=(8, 5))
sns.barplot(x='Personal_Loan', y='CCAvg', data=data, estimator=lambda x: x.mean(), palette='Blues')
plt.xlabel('Personal Loan Status')
plt.ylabel('Average Income')
plt.title('Average Income by Personal Loan Status')
plt.xticks([0, 1], ['No Loan', 'Loan']) # Labeling the x-axis for clarity
plt.show()
data["Experience"].unique()
# checking for experience <0
data[data["Experience"] < 0]["Experience"].unique()
# Correcting the experience values
data["Experience"].replace(-1, 1, inplace=True)
data["Experience"].replace(-2, 2, inplace=True)
data["Experience"].replace(-3, 3, inplace=True)
data["Education"].unique()
# checking the number of uniques in the zip code
data["ZIPCode"].nunique()
data["ZIPCode"] = data["ZIPCode"].astype(str)
print(
"Number of unique values if we take first two digits of ZIPCode: ",
data["ZIPCode"].str[0:2].nunique(),
)
data["ZIPCode"] = data["ZIPCode"].str[0:2]
data["ZIPCode"] = data["ZIPCode"].astype("category")
# Find the the outliers with Interquartile Range (IQR).
# Select only numerical columns
numerical_data = data.select_dtypes(include=['float64', 'int64'])
Q1 = numerical_data.quantile(0.25)
Q3 = numerical_data.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
((numerical_data < lower) | (numerical_data > upper)).sum() / len(numerical_data) * 100
# Find the the outliers with Interquartile Range (IQR).
# Select only numerical columns
numerical_data = data.select_dtypes(include=['float64', 'int64'])
Q1 = numerical_data.quantile(0.25)
Q3 = numerical_data.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
((numerical_data < lower) | (numerical_data > upper)).sum() / len(numerical_data) * 100
# dropping Experience as it is perfectly correlated with Age
X = data.drop(["Personal_Loan", "Experience"], axis=1)
Y = data["Personal_Loan"]
X = pd.get_dummies(X, columns=["ZIPCode", "Education"], drop_first=True)
# Splitting data in train and test sets
X_train, X_test, y_train, y_test = train_test_split(
X, Y, test_size=0.30, random_state=1
)
print("Shape of Training set : ", X_train.shape)
print("Shape of test set : ", X_test.shape)
print("Percentage of classes in training set:")
print(y_train.value_counts(normalize=True))
print("Percentage of classes in test set:")
print(y_test.value_counts(normalize=True))
*
# defining a function to compute different metrics to check performance of a classification model built using sklearn
def model_performance_classification_sklearn(model, predictors, target):
"""
Function to compute different metrics to check classification model performance
model: classifier
predictors: independent variables
target: dependent variable
"""
# predicting using the independent variables
pred = model.predict(predictors)
acc = accuracy_score(target, pred) # to compute Accuracy
recall = recall_score(target, pred) # to compute Recall
precision = precision_score(target, pred) # to compute Precision
f1 = f1_score(target, pred) # to compute F1-score
# creating a dataframe of metrics
df_perf = pd.DataFrame(
{"Accuracy": acc, "Recall": recall, "Precision": precision, "F1": f1,},
index=[0],
)
return df_perf
def confusion_matrix_sklearn(model, predictors, target):
"""
To plot the confusion_matrix with percentages
model: classifier
predictors: independent variables
target: dependent variable
"""
y_pred = model.predict(predictors)
cm = confusion_matrix(target, y_pred)
labels = np.asarray(
[
["{0:0.0f}".format(item) + "\n{0:.2%}".format(item / cm.flatten().sum())]
for item in cm.flatten()
]
).reshape(2, 2)
plt.figure(figsize=(6, 4))
sns.heatmap(cm, annot=labels, fmt="")
plt.ylabel("True label")
plt.xlabel("Predicted label")
# Encode categorical variables using one-hot encoding.
X_train_encoded = pd.get_dummies(X_train)
X_test_encoded = pd.get_dummies(X_test)
# Ensure the training and test sets have the same columns after encoding.
X_train_encoded, X_test_encoded = X_train_encoded.align(X_test_encoded, join='left', axis=1, fill_value=0)
# Check for NaN values
print(X_train_encoded.isnull().sum().sum()) # Should print 0 if there are no NaN values
print(X_test_encoded.isnull().sum().sum()) # Should print 0 if there are no NaN values
# If there are NaN values, you can fill them (though they shouldn't be any after the align step):
X_train_encoded = X_train_encoded.fillna(0)
X_test_encoded = X_test_encoded.fillna(0)
# Ensure all columns are numeric
print(X_train_encoded.dtypes) # Should show all columns as int64 or float64
print(X_test_encoded.dtypes) # Should show all columns as int64 or float64
# Drop redundant columns
X_train_encoded = X_train_encoded.loc[:, ~X_train_encoded.columns.duplicated()]
X_test_encoded = X_test_encoded.loc[:, ~X_test_encoded.columns.duplicated()]
# Verify column consistency
if set(X_train_encoded.columns) != set(X_test_encoded.columns):
raise ValueError("Training and test datasets have different columns.")
# Fit the model
dtree1 = DecisionTreeClassifier(random_state=42) # random_state sets a seed value and enables reproducibility
dtree1.fit(X_train_encoded, y_train)
confusion_matrix_sklearn(dtree1, X_train_encoded, y_train)
dtree1_train_perf = model_performance_classification_sklearn(
dtree1, X_train_encoded, y_train
)
dtree1_train_perf
dtree1_test_perf = model_performance_classification_sklearn(
dtree1, X_test_encoded, y_test
)
dtree1_test_perf
feature_names = list(X_train_encoded.columns)
print(feature_names)
# list of feature names in X_train
feature_names = list(X_train_encoded.columns)
# set the figure size for the plot
plt.figure(figsize=(20, 20))
# plotting the decision tree
out = tree.plot_tree(
dtree1, # decision tree classifier model
feature_names=feature_names, # list of feature names (columns) in the dataset
filled=True, # fill the nodes with colors based on class
fontsize=9, # font size for the node text
node_ids=False, # do not show the ID of each node
class_names=None, # whether or not to display class names
)
# add arrows to the decision tree splits if they are missing
for o in out:
arrow = o.arrow_patch
if arrow is not None:
arrow.set_edgecolor("black") # set arrow color to black
arrow.set_linewidth(1) # set arrow linewidth to 1
# displaying the plot
plt.show()
# define the parameters of the tree to iterate over
max_depth_values = np.arange(2, 11, 2)
max_leaf_nodes_values = np.arange(10, 51, 10)
min_samples_split_values = np.arange(10, 51, 10)
# initialize variables to store the best model and its performance
best_estimator = None
best_score_diff = float('inf')
# iterate over all combinations of the specified parameter values
for max_depth in max_depth_values:
for max_leaf_nodes in max_leaf_nodes_values:
for min_samples_split in min_samples_split_values:
# initialize the tree with the current set of parameters
estimator = DecisionTreeClassifier(
max_depth=max_depth,
max_leaf_nodes=max_leaf_nodes,
min_samples_split=min_samples_split,
random_state=42
)
# fit the model to the training data
estimator.fit(X_train_encoded, y_train)
# make predictions on the training and test sets
y_train_pred = estimator.predict(X_train_encoded)
y_test_pred = estimator.predict(X_test_encoded)
# calculate F1 scores for training and test sets
train_f1_score = f1_score(y_train, y_train_pred)
test_f1_score = f1_score(y_test, y_test_pred)
# calculate the absolute difference between training and test F1 scores
score_diff = abs(train_f1_score - test_f1_score)
# update the best estimator and best score if the current one has a smaller score difference
if score_diff < best_score_diff:
best_score_diff = score_diff
best_estimator = estimator
# creating an instance of the best model
dtree2 = best_estimator
# fitting the best model to the training data
dtree2.fit(X_train_encoded, y_train)
confusion_matrix_sklearn(dtree2, X_train_encoded, y_train)
dtree2_train_perf = model_performance_classification_sklearn(
dtree2, X_train_encoded, y_train
)
dtree2_train_perf
confusion_matrix_sklearn(dtree2, X_test_encoded, y_test)
dtree2_test_perf = model_performance_classification_sklearn(
dtree2, X_test_encoded, y_test
)
dtree2_test_perf
# list of feature names in X_train
feature_names = list(X_train_encoded.columns)
# set the figure size for the plot
plt.figure(figsize=(20, 20))
# plotting the decision tree
out = tree.plot_tree(
dtree2, # decision tree classifier model
feature_names=feature_names, # list of feature names (columns) in the dataset
filled=True, # fill the nodes with colors based on class
fontsize=9, # font size for the node text
node_ids=False, # do not show the ID of each node
class_names=None, # whether or not to display class names
)
# add arrows to the decision tree splits if they are missing
for o in out:
arrow = o.arrow_patch
if arrow is not None:
arrow.set_edgecolor("black") # set arrow color to black
arrow.set_linewidth(1) # set arrow linewidth to 1
# displaying the plot
plt.show()
# printing a text report showing the rules of a decision tree
print(
tree.export_text(
dtree2, # specify the model
feature_names=feature_names, # specify the feature names
show_weights=True # specify whether or not to show the weights associated with the model
)
)
# Create an instance of the decision tree model
clf = DecisionTreeClassifier(random_state=42)
# Compute the cost complexity pruning path for the model using the training data
path = clf.cost_complexity_pruning_path(X_train_encoded, y_train)
# Extract the array of effective alphas from the pruning path
ccp_alphas = abs(path.ccp_alphas)
# Extract the array of total impurities at each alpha along the pruning path
impurities = path.impurities
pd.DataFrame(path)
# Create a figure
fig, ax = plt.subplots(figsize=(10, 5))
# Plot the total impurities versus effective alphas, excluding the last value,
# using markers at each data point and connecting them with steps
ax.plot(ccp_alphas[:-1], impurities[:-1], marker="o", drawstyle="steps-post")
# Set the x-axis label
ax.set_xlabel("Effective Alpha")
# Set the y-axis label
ax.set_ylabel("Total impurity of leaves")
# Set the title of the plot
ax.set_title("Total Impurity vs Effective Alpha for training set");
# Initialize an empty list to store the decision tree classifiers
clfs = []
# Iterate over each ccp_alpha value extracted from cost complexity pruning path
for ccp_alpha in ccp_alphas:
# Create an instance of the DecisionTreeClassifier
clf = DecisionTreeClassifier(ccp_alpha=ccp_alpha, random_state=42)
# Fit the classifier to the training data
clf.fit(X_train_encoded, y_train)
# Append the trained classifier to the list
clfs.append(clf)
# Print the number of nodes in the last tree along with its ccp_alpha value
print(
"Number of nodes in the last tree is {} with ccp_alpha {}".format(
clfs[-1].tree_.node_count, ccp_alphas[-1]
)
)
# Remove the last classifier and corresponding ccp_alpha value from the lists
clfs = clfs[:-1]
ccp_alphas = ccp_alphas[:-1]
# Extract the number of nodes in each tree classifier
node_counts = [clf.tree_.node_count for clf in clfs]
# Extract the maximum depth of each tree classifier
depth = [clf.tree_.max_depth for clf in clfs]
# Create a figure and a set of subplots
fig, ax = plt.subplots(2, 1, figsize=(10, 7))
# Plot the number of nodes versus ccp_alphas on the first subplot
ax[0].plot(ccp_alphas, node_counts, marker="o", drawstyle="steps-post")
ax[0].set_xlabel("Alpha")
ax[0].set_ylabel("Number of nodes")
ax[0].set_title("Number of nodes vs Alpha")
# Plot the depth of tree versus ccp_alphas on the second subplot
ax[1].plot(ccp_alphas, depth, marker="o", drawstyle="steps-post")
ax[1].set_xlabel("Alpha")
ax[1].set_ylabel("Depth of tree")
ax[1].set_title("Depth vs Alpha")
# Adjust the layout of the subplots to avoid overlap
fig.tight_layout()
train_f1_scores = [] # Initialize an empty list to store F1 scores for training set for each decision tree classifier
# Iterate through each decision tree classifier in 'clfs'
for clf in clfs:
# Predict labels for the training set using the current decision tree classifier
pred_train = clf.predict(X_train_encoded)
# Calculate the F1 score for the training set predictions compared to true labels
f1_train = f1_score(y_train, pred_train)
# Append the calculated F1 score to the train_f1_scores list
train_f1_scores.append(f1_train)
test_f1_scores = [] # Initialize an empty list to store F1 scores for test set for each decision tree classifier
# Iterate through each decision tree classifier in 'clfs'
for clf in clfs:
# Predict labels for the test set using the current decision tree classifier
pred_test = clf.predict(X_test_encoded)
# Calculate the F1 score for the test set predictions compared to true labels
f1_test = f1_score(y_test, pred_test)
# Append the calculated F1 score to the test_f1_scores list
test_f1_scores.append(f1_test)
# Create a figure
fig, ax = plt.subplots(figsize=(15, 5))
ax.set_xlabel("Alpha") # Set the label for the x-axis
ax.set_ylabel("F1 Score") # Set the label for the y-axis
ax.set_title("F1 Score vs Alpha for training and test sets") # Set the title of the plot
# Plot the training F1 scores against alpha, using circles as markers and steps-post style
ax.plot(ccp_alphas, train_f1_scores, marker="o", label="training", drawstyle="steps-post")
# Plot the testing F1 scores against alpha, using circles as markers and steps-post style
ax.plot(ccp_alphas, test_f1_scores, marker="o", label="test", drawstyle="steps-post")
ax.legend(); # Add a legend to the plot
# creating the model where we get highest test F1 Score
index_best_model = np.argmax(test_f1_scores)
# selcting the decision tree model corresponding to the highest test score
dtree3 = clfs[index_best_model]
print(dtree3)
confusion_matrix_sklearn(dtree3, X_train_encoded, y_train)
dtree3_train_perf = model_performance_classification_sklearn(
dtree3, X_train_encoded, y_train
)
dtree3_train_perf
confusion_matrix_sklearn(dtree3, X_test_encoded, y_test)
dtree3_test_perf = model_performance_classification_sklearn(
dtree3, X_test_encoded, y_test
)
dtree3_test_perf
# list of feature names in X_train
feature_names = list(X_train_encoded.columns)
# set the figure size for the plot
plt.figure(figsize=(10, 7))
# plotting the decision tree
out = tree.plot_tree(
dtree3, # decision tree classifier model
feature_names=feature_names, # list of feature names (columns) in the dataset
filled=True, # fill the nodes with colors based on class
fontsize=9, # font size for the node text
node_ids=False, # do not show the ID of each node
class_names=None, # whether or not to display class names
)
# add arrows to the decision tree splits if they are missing
for o in out:
arrow = o.arrow_patch
if arrow is not None:
arrow.set_edgecolor("black") # set arrow color to black
arrow.set_linewidth(1) # set arrow linewidth to 1
# displaying the plot
plt.show()
# training performance comparison
models_train_comp_df = pd.concat(
[
dtree1_train_perf.T,
dtree2_train_perf.T,
dtree3_train_perf.T,
],
axis=1,
)
models_train_comp_df.columns = [
"Decision Tree (sklearn default)",
"Decision Tree (Pre-Pruning)",
"Decision Tree (Post-Pruning)",
]
print("Training performance comparison:")
models_train_comp_df
# testing performance comparison
models_test_comp_df = pd.concat(
[
dtree1_test_perf.T,
dtree2_test_perf.T,
dtree3_test_perf.T,
],
axis=1,
)
models_test_comp_df.columns = [
"Decision Tree (sklearn default)",
"Decision Tree (Pre-Pruning)",
"Decision Tree (Post-Pruning)",
]
print("Test set performance comparison:")
models_test_comp_df
# importance of features in the tree building
importances = dtree2.feature_importances_
indices = np.argsort(importances)
plt.figure(figsize=(8, 8))
plt.title("Feature Importances")
plt.barh(range(len(indices)), importances[indices], color="violet", align="center")
plt.yticks(range(len(indices)), [feature_names[i] for i in indices])
plt.xlabel("Relative Importance")
plt.show()
1 )Focus marketing on high-income individuals with offers tailored to their needs, like loans for investments or luxury purchases.
2 )Family-Focused Loan Products: Create loan products with incentives, such as lower interest rates, specifically designed for larger families.
3 ) Education-Based Loan Offers: Develop loan products for customers with higher education, targeting their needs for professional development or continuing education.
4 ) Cross-Sell to CD and Credit Card Holders: Encourage CD account holders and active credit card users to take personal loans by offering bundled deals and loyalty rewards.
5) Customer Education Programs: Offer educational webinars to help customers understand the benefits of personal loans, making them more likely to consider these products.